package com.andova.glcamera.device

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.ImageFormat
import android.graphics.SurfaceTexture
import android.hardware.camera2.*
import android.hardware.camera2.params.StreamConfigurationMap
import android.media.Image
import android.media.ImageReader
import android.media.MediaRecorder
import android.os.Handler
import android.os.HandlerThread
import android.os.Message
import android.util.Range
import android.util.SparseIntArray
import android.view.Surface
import androidx.annotation.RequiresApi
import com.andova.egl.OnTextureBridger
import com.andova.ext.d
import com.andova.ext.e
import com.andova.ext.i
import com.andova.glcamera.FRAME_ACTION_LAZY_PREVIEW
import com.andova.glcamera.FRAME_ACTION_PREVIEW
import com.andova.glcamera.pool.HT_NAME_PIPELINE
import com.andova.image.ImagePlane
import com.andova.image.decodeToOriginalBitmap
import com.andova.libyuv.LibYUV
import com.andova.pipeline.PipelineHandler
import kotlinx.coroutines.*
import java.io.File
import java.util.*
import java.util.concurrent.locks.ReentrantLock

@RequiresApi(21)
class Camera2Device(context: Context) : CameraCallback {
    private val mCameraManager = context.getSystemService(Context.CAMERA_SERVICE) as? CameraManager
    private var mPreviewRequestBuilder: CaptureRequest.Builder? = null
    private var mCameraCharacteristics: CameraCharacteristics? = null
    private var builder: CameraBuilder? = null
    private var mImageReader: ImageReader? = null

    private var mCamera: CameraDevice? = null
    private var mRender: OnTextureBridger? = null
    private var mSurfaceTexture: SurfaceTexture? = null
    private var mCaptureSession: CameraCaptureSession? = null

    private val internalFacings = SparseIntArray()
    private var mFacing = getCameraId()
    private var mCameraId = mFacing.toString()
    private var mPreviewSize: Size? = null
    private var mPictureSize: Size? = null
    private val mPreviewSizes: SizeMap = SizeMap()
    private val mPictureSizes: SizeMap = SizeMap()

    private var mHandler: PipelineHandler? = null
    private val mBufferLock = ReentrantLock() // 加锁确保y、u、v来源于同一个Image
    private var mediaRecorder: MediaRecorder? = null
    private val mImagePlane = ImagePlane()

    private var openRetry = false
    private fun retryOpenCamera() {
        if (mSurfaceTexture == null || builder == null) return
        if (openRetry) return
        openRetry = true
        i("retry open camera again...")
        openCamera(mSurfaceTexture, builder)
    }

    private val mCameraDeviceCallback: CameraDevice.StateCallback = object : CameraDevice.StateCallback() {
        override fun onOpened(camera: CameraDevice) {
            i("onOpened: ${camera.id}")
            builder?.stateCallback()?.onOpened(camera.id)
            mCamera = camera
            openRetry = false
            startCaptureSession(false, mSurfaceTexture)
        }

        override fun onClosed(camera: CameraDevice) {
            i("onClosed: ${camera.id}")
            builder?.stateCallback()?.onClosed(camera.id)
            mCamera = null
            release()
        }

        override fun onDisconnected(camera: CameraDevice) {
            e("onDisconnected: ${camera.id}")
            builder?.stateCallback()?.onDisconnected(camera.id)
            mCamera = null
        }

        override fun onError(camera: CameraDevice, error: Int) {
            e("onError: ${camera.id} ($error)")
            builder?.stateCallback()?.onError(camera.id)
            mCamera = null
            retryOpenCamera()
        }
    }
    private val mSessionCallback: CameraCaptureSession.StateCallback = object : CameraCaptureSession.StateCallback() {
        override fun onConfigured(session: CameraCaptureSession) {
            if (mCamera == null) {
                return
            }
            mCaptureSession = session
//            updateAutoFocus()
//            updateFlash()
            try {
                mCaptureSession?.setRepeatingRequest(mPreviewRequestBuilder?.build() ?: return, null, null)
            } catch (e: CameraAccessException) {
                e("Failed to start camera preview because it couldn't access camera. $e")
            } catch (e: IllegalStateException) {
                e("Failed to start camera preview. $e")
            }
        }

        override fun onConfigureFailed(session: CameraCaptureSession) {
            e("Failed to configure capture session.")
        }

        override fun onClosed(session: CameraCaptureSession) {
            if (mCaptureSession != null && mCaptureSession == session) {
                mCaptureSession = null
            }
        }
    }
    private val mOnImageAvailableListener = ImageReader.OnImageAvailableListener { reader ->
        var image: Image? = null
        try {
            image = reader.acquireNextImage()
            when (mRender?.obtainAction()) {
                FRAME_ACTION_PREVIEW, FRAME_ACTION_LAZY_PREVIEW -> return@OnImageAvailableListener
            }

            mBufferLock.lock()
            val dst: ByteArray?
            val angle = builder?.frameAngle ?: ANGLE_0
            if (angle != ANGLE_0) {
                val src = mImagePlane.YUVToNV21(image) ?: return@OnImageAvailableListener
                dst = ByteArray(src.size)
                LibYUV.RotateNV21(src, dst, angle, image.planes[0].rowStride, image.height)
            } else dst = mImagePlane.YUVToNV21(image)
            if (mHandler?.isBusy() == false) {
                val msg = Message.obtain()
                msg.obj = dst
                mHandler?.sendMessage(msg)
            }
            mBufferLock.unlock()
        } catch (e: Exception) {
            mBufferLock.unlock()
        } finally {
            image?.close()
        }
    }

    init {
        internalFacings.put(CAMERA_ID_0, CameraCharacteristics.LENS_FACING_BACK)
        internalFacings.put(CAMERA_ID_1, CameraCharacteristics.LENS_FACING_FRONT)
    }

    private var mPreviewHandler: Handler? = null
    private var mPreviewThread: HandlerThread? = null

    private fun startBackgroundThread() {
        mPreviewThread = HandlerThread("Camera${mCameraId}Preview").apply { start() }
        mPreviewHandler = Handler(mPreviewThread!!.looper)
    }

    private fun stopBackgroundThread() {
        try {
            mPreviewThread?.quit()
            mPreviewHandler?.removeCallbacksAndMessages(null)
            mPreviewHandler = null
        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
    }

    /**
     *
     * Chooses a camera ID by the specified camera facing ([.mFacing]).
     *
     * This rewrites [.mCameraId], [.mCameraCharacteristics], and optionally
     * [.mFacing].
     */
    private fun chooseCameraIdByFacing(): Boolean {
        return try {
            val internalFacing: Int = internalFacings.get(getCameraId())
            val ids = mCameraManager?.cameraIdList
            if (ids.isNullOrEmpty()) { // No camera
                throw RuntimeException("No camera available.")
            }
            for (id in ids) {
                val characteristics = mCameraManager?.getCameraCharacteristics(id) ?: return false
                val level = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
                if (level == null || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
                    continue
                }
                val internal = characteristics.get(CameraCharacteristics.LENS_FACING) ?: throw NullPointerException("Unexpected state: LENS_FACING null")
                if (internal == internalFacing) {
                    mCameraId = id
                    mCameraCharacteristics = characteristics
                    return true
                }
            }
            // Not found
            mCameraId = ids[0]
            mCameraCharacteristics = mCameraManager?.getCameraCharacteristics(mCameraId)
            val level = mCameraCharacteristics?.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
            if (level == null || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
                return false
            }
            val internal = mCameraCharacteristics?.get(CameraCharacteristics.LENS_FACING) ?: throw NullPointerException("Unexpected state: LENS_FACING null")
            var i = 0
            val count: Int = internalFacings.size()
            while (i < count) {
                if (internalFacings.valueAt(i) == internal) {
                    mFacing = internalFacings.keyAt(i)
                    return true
                }
                i++
            }
            // The operation can reach here when the only camera device is an external one.
            // We treat it as facing back.
            mFacing = CAMERA_ID_0
            true
        } catch (e: CameraAccessException) {
            throw RuntimeException("Failed to get a list of camera devices", e)
        }
    }

    /**
     *
     * Collects some information from [.mCameraCharacteristics].
     *
     * This rewrites [.mPreviewSizes], [.mPictureSizes], and optionally,
     * [.mAspectRatio].
     */
    private fun collectCameraInfo() {
        val map: StreamConfigurationMap = mCameraCharacteristics?.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: return
        val fpsRanges: Array<Range<Int>> = mCameraCharacteristics?.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) ?: return
        d("SYNC_MAX_LATENCY_PER_FRAME_CONTROL: ${fpsRanges.contentToString()}")
        mPreviewSizes.clear()
        for (size in map.getOutputSizes(SurfaceTexture::class.java)) {
            val width = size.width
            val height = size.height
            if (width <= MAX_PREVIEW_WIDTH && height <= MAX_PREVIEW_HEIGHT) {
                mPreviewSizes.add(Size(width, height))
            }
        }
        mPictureSizes.clear()
        collectPictureSizes(map)
        for (ratio in mPreviewSizes.ratios()) {
            if (!mPictureSizes.ratios().contains(ratio)) {
                mPreviewSizes.remove(ratio)
            }
        }
        if (!mPreviewSizes.ratios().contains(getAspectRatio())) {
            builder?.aspectRatio(mPreviewSizes.ratios().iterator().next())
        }
    }

    private fun collectPictureSizes(map: StreamConfigurationMap) {
        for (size in map.getOutputSizes(ImageFormat.YUV_420_888)) {
            mPictureSizes.add(Size(size.width, size.height))
        }
    }

    private fun prepareImageReader() {
        mImageReader?.close()
        val ss = mPictureSizes.sizes(getAspectRatio())
        d("Camera picture size list -> $ss")
        val size: Size = builder?.pictureSize(ss) ?: ss.last()
        mPictureSize = size
        mImageReader = ImageReader.newInstance(size.width, size.height, ImageFormat.YUV_420_888,  /* maxImages */1)
        mImageReader?.setOnImageAvailableListener(mOnImageAvailableListener, mPreviewHandler)
    }

    /**
     *
     * Starts opening a camera device.
     *
     * The result will be processed in [.mCameraDeviceCallback].
     */
    @SuppressLint("MissingPermission")
    private fun startOpeningCamera() {
        try {
            mCameraManager?.openCamera(mCameraId, mCameraDeviceCallback, mPreviewHandler)
        } catch (e: CameraAccessException) {
            throw java.lang.RuntimeException("Failed to open camera: $mCameraId", e)
        }
    }


    private fun closeCaptureSession() = mCaptureSession?.close()

    /**
     *
     * Starts a capture session for camera preview.
     *
     * This rewrites [.mPreviewRequestBuilder].
     *
     * The result will be continuously processed in [.mSessionCallback].
     */
    fun startCaptureSession(isRecord: Boolean, surfaceTexture: SurfaceTexture?) {
        if (mCamera == null) return
        closeCaptureSession()
        try {
            val ps = mPreviewSizes.sizes(getAspectRatio())
            d("Camera preview size list -> $ps")
            val size = builder?.previewSize(ps) ?: ps.last()
            mPreviewSize = size
            surfaceTexture?.setDefaultBufferSize(size.width, size.height)
            val templateType = if (isRecord) CameraDevice.TEMPLATE_RECORD else CameraDevice.TEMPLATE_PREVIEW
            mPreviewRequestBuilder = mCamera?.createCaptureRequest(templateType)
            val surfaces = collectSurfaces(isRecord, surfaceTexture)
            for (surface in surfaces) mPreviewRequestBuilder?.addTarget(surface)
            mCamera?.createCaptureSession(surfaces, mSessionCallback, mPreviewHandler)
        } catch (e: CameraAccessException) {
            throw RuntimeException("Failed to start camera session")
        }
    }

    private fun collectSurfaces(isRecord: Boolean, surfaceTexture: SurfaceTexture?): List<Surface> {
        val list = mutableListOf<Surface>()
        surfaceTexture?.apply { list.add(Surface(this)) }
        when {
            isRecord -> mediaRecorder?.apply { list.add(surface) }
            builder?.usePreviewCallback == true -> mImageReader?.surface?.apply { list.add(this) }
        }
        return list
    }

    /**
     * Chooses the optimal preview size based on [.mPreviewSizes] and the surface size.
     *
     * @return The picked size for camera preview.
     */
    private fun chooseOptimalSize(surfaceWidth: Int, surfaceHeight: Int): Size? {
        val surfaceLonger: Int
        val surfaceShorter: Int
        if (surfaceWidth < surfaceHeight) {
            surfaceLonger = surfaceHeight
            surfaceShorter = surfaceWidth
        } else {
            surfaceLonger = surfaceWidth
            surfaceShorter = surfaceHeight
        }
        val candidates: SortedSet<Size> = mPreviewSizes.sizes(getAspectRatio())

        // Pick the smallest of those big enough
        for (size in candidates) {
            if (size.width >= surfaceLonger && size.height >= surfaceShorter) {
                return size
            }
        }
        // If no size is big enough, pick the largest one.
        return candidates.last()
    }

    override fun openCamera(surfaceTexture: SurfaceTexture?, builder: CameraBuilder?) {
        this.builder = builder
        mSurfaceTexture = surfaceTexture
        if (!chooseCameraIdByFacing()) {
            mCameraId = getCameraId().toString()
            try {
                mCameraCharacteristics = mCameraManager?.getCameraCharacteristics(mCameraId)
            } catch (ignore: CameraAccessException) {
                throw RuntimeException("Failed to open camera")
            }
        }
        startBackgroundThread()
        collectCameraInfo()
        prepareImageReader()
        startOpeningCamera()
    }

    override fun stopPreview() {
        mCamera?.close()
        mCamera = null
    }

    override fun release() {
        mImageReader?.close()
        mImageReader = null
        closeCaptureSession()
        mCaptureSession = null
        stopBackgroundThread()
    }

    override fun setFrameBufferUpdated(value: Boolean) {
        // todo something
    }

    override fun isFrameBufferUpdated(): Boolean = true
    override fun getPreviewSize(): Size = mPreviewSize ?: Size(0, 0)
    override fun getPictureSize(): Size = mPictureSize ?: Size(0, 0)
    override fun getBuilder(): CameraBuilder? = builder
    override fun setBuilder(builder: CameraBuilder?) = run { this.builder = builder }
    override fun getCameraId(): Int = builder?.cameraId ?: CAMERA_ID_0
    override fun getAspectRatio(): AspectRatio = builder?.aspectRatio ?: ASPECT_RATIO_16_9
    override fun isCameraOpened(): Boolean = mCamera != null
    override fun setBridger(bridger: OnTextureBridger?) {
        mRender = bridger
        mHandler = PipelineHandler(bridger ?: return).apply {
            start("$HT_NAME_PIPELINE$mCameraId")
        }
    }

    override fun takePicture(out: File?, callback: (dst: File?) -> Unit) {
        out ?: return callback(null)
        GlobalScope.launch {
            if (mPictureSize == null) return@launch withContext(Dispatchers.Main) { callback(null) }
            val nv21 = mHandler?.getBuffer() ?: return@launch withContext(Dispatchers.Main) { callback(null) }
            val dst = nv21.clone()
            val byte0 = 0.toByte()
            var valid = false
            for (n in dst) if (n != byte0) {
                valid = true
                break
            }
            if (!valid) return@launch withContext(Dispatchers.Main) { callback(null) }
            closeCaptureSession()
            val width = mPictureSize!!.width
            val height = mPictureSize!!.height
            when (builder?.frameAngle) {
                ANGLE_90, ANGLE_270 -> decodeToOriginalBitmap(dst, out, height, width)
                else -> decodeToOriginalBitmap(dst, out, width, height)
            }
            withContext(Dispatchers.Main) { callback(if (out.exists()) out else null) }
        }
    }

    override fun startRecord(builder: RecorderBuilder) {
        mediaRecorder = MediaRecorder().apply {
            setVideoSource(MediaRecorder.VideoSource.SURFACE)
            setAudioSource(MediaRecorder.AudioSource.MIC)
            setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
            setVideoEncoder(MediaRecorder.VideoEncoder.H264)
            setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
            val width = mPreviewSize?.width ?: 0
            val height = mPreviewSize?.height ?: 0
            setVideoSize(width, height)
            setVideoEncodingBitRate(8 * width * height)
            builder.apply {
                setOrientationHint(orientationHint)
                setVideoFrameRate(frameRate)
                setOutputFile(out)
            }
            prepare()
        }
        startCaptureSession(true, mSurfaceTexture)
        mediaRecorder?.start()
    }

    override fun stopRecord() {
        closeCaptureSession()
        mediaRecorder?.apply {
            setOnErrorListener(null)
            setOnInfoListener(null)
            setPreviewDisplay(null)
        }
        try {
            mediaRecorder?.stop()
        } catch (e: RuntimeException) {
            e.printStackTrace()
        } finally {
            mediaRecorder?.release()
            mediaRecorder = null
        }
    }

    override fun resetCaptureSession() = startCaptureSession(false, mSurfaceTexture)
    override fun getSurfaceTexture(): SurfaceTexture? = mSurfaceTexture
}